/*
 * Storage is designed as an abstraction for I/O operations. Besides, it also provides utility functions 
 * such as isTaskDone that are used to check the task states.
 *  
 */
/** @author A0105514U */
// @author A0105514U
package storage;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;

import global.GlobalFields;
import global.Task;
import global.Constant;

import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Logger;
import java.util.logging.Level;

public class Storage {
	private static final String TEMP_FILE_STRING = "temp";
	private static String pathSeparator;
	private static File storageFolder;
	private static String storagePath;
	private static ArrayList<ModificationRecord> modRecord;
	
	private static String storageLogFileName = "storageLog.txt";
	private static Logger logger;
	private static FileHandler fh;
	
	// This is where the commands that are unsynced to Google Calendar will be kept
	// when the system exits. 
	private static File commandStorage;
	
	// Initialize the Storage
	public static void init(){
		
		try{
			logger = Logger.getLogger(Storage.class.getName());
			fh = new FileHandler(storageLogFileName);
			logger.addHandler(fh);
			
			determineSystemPathSeparator();
			generateTaskStoragePath();
			createStorageFolder();
			constructModificationRecord();
			constructCommandStorage();
			addCommandsInStore();
			
			logger.log(Level.INFO,"Enterring Storage");
			logger.log(Level.INFO,"Initializing...");
		}catch(Exception e){
			logger.log(Level.SEVERE,"Storage init failed");
		}
	}
	
	private static void constructModificationRecord(){
		modRecord = new ArrayList<ModificationRecord>();
	}
	
	private static void constructCommandStorage() throws IOException {
		String commandStoragePath = determineCommandStorageFilePath();
		commandStorage = new File(commandStoragePath);

		if(!commandStorage.exists()){
			logger.log(Level.INFO,"Command storage does not exist");
			commandStorage = createCommandStorage(commandStoragePath);
		}
	}
	
	// Create a command storage file to store commands that are not sync in this run of the system
	private static File createCommandStorage(String commandStoragePath) throws IOException{
		File cmdStorage;
		
		String cmdStorageFolderPath = determineCommandStorageFolderPath();
		File cmdStorageFolder = new File(cmdStorageFolderPath);
		
		if(!cmdStorageFolder.exists() || !cmdStorageFolder.isDirectory()){
			cmdStorageFolder.mkdir();
		}
		
		cmdStorage = new File(commandStoragePath);
		cmdStorage.createNewFile();
		
		return cmdStorage;
	}
	
	private static String determineCommandStorageFolderPath(){
		String path = storagePath+pathSeparator+Constant.MOD_RECORD_FOLDER_NAME;
		logger.log(Level.INFO,"CommandStorage Folder Path is "+path);
		return path;
	}
	
	private static String determineCommandStorageFilePath(){
		String path = storagePath+pathSeparator+Constant.MOD_RECORD_FOLDER_NAME + pathSeparator+Constant.MOD_RECORD_FILE_NAME;
		logger.log(Level.INFO,"CommandStorage File Path is "+path);
		return path;
	}
	
	// Add the commands stored in the commandStorage file into modification record so as to sync them as long as
	// Google connection is established.
	// These are commands that are left unsync the last time the program exits.
	private static void addCommandsInStore() throws IOException {
		BufferedReader reader = new BufferedReader(new FileReader(commandStorage));
		String line = reader.readLine();
		
		while(line != null){
			addRecord(line);
			line = reader.readLine();
		}
		
		reader.close();
	}
	
	private static void addRecord(String line){
		ModificationRecord newRecord = null;
		
		try{
			String cmdType,addedTaskFileName,deletedTaskId;
			String[] records = line.split(Constant.HEX);
			cmdType = records[0];
			addedTaskFileName = records[1];
			
			// If it is a delete command, after the split records will only have length 2
			// since the last element is going to be an empty string, which will not be detected
			// Check this to avoid IndexOutOfBound Exception and assign the correct value to deletedTaskId
			if(cmdType.equalsIgnoreCase(Constant.DELETE_COMMAND_TYPE) && records.length >= 3){
				deletedTaskId = records[2];
			}else{
				deletedTaskId = Constant.EMPTY_STRING;
			}
			
			newRecord = new ModificationRecord(cmdType,addedTaskFileName,deletedTaskId);
		}catch(Exception e){
			// do nothing
		}
		
		if(newRecord != null){
			logger.log(Level.INFO,"Adding record to modRecord: "+newRecord);
			modRecord.add(newRecord);
		}
	}
	
	private static void generateTaskStoragePath(){
		assert(pathSeparator.equalsIgnoreCase(Constant.PATH_SEPARATOR_FOR_WINDOWS) || 
				pathSeparator.equalsIgnoreCase(Constant.PATH_SEPARATOR_FOR_UNIX));
		
		String systemUserName = System.getProperty("user.name");
		
		// Cases for two different directories are established, one for Windows and the other for Mac OS
		if(pathSeparator.equalsIgnoreCase(Constant.PATH_SEPARATOR_FOR_WINDOWS)){	
			String systemPath = Constant.DRIVE_PARTITION+pathSeparator+Constant.USERS+pathSeparator+systemUserName+pathSeparator+Constant.DOCUMENTS;
			storagePath = systemPath+pathSeparator+Constant.TASK_STORAGE_FOLDER_NAME;
		}else if(pathSeparator.equalsIgnoreCase(Constant.PATH_SEPARATOR_FOR_UNIX)){
			storagePath = pathSeparator+Constant.USERS+pathSeparator+systemUserName+pathSeparator+Constant.DOCUMENTS+pathSeparator+Constant.TASK_STORAGE_FOLDER_NAME;
		}
		
		logger.log(Level.INFO,"Storage path is determined as "+storagePath);
	}
	
	private static void determineSystemPathSeparator(){
		try{
			String tempFileName = TEMP_FILE_STRING;
			File tempFile = new File(tempFileName);
			String path = tempFile.getAbsolutePath();
			
			// path separator could be \\ or / depending on what system the software is running on
			if(path.length() != 0){
				if(path.contains(Constant.PATH_SEPARATOR_FOR_WINDOWS)){
					pathSeparator = Constant.PATH_SEPARATOR_FOR_WINDOWS;
				}else{
					pathSeparator = Constant.PATH_SEPARATOR_FOR_UNIX;
				}
			}
			
			tempFile.delete();
		}catch(Exception e){
			// do nothing
		}
	}
	
	private static void createStorageFolder(){
		storageFolder = new File(storagePath);
		storageFolder.mkdir();
	}
	
	// Returns a list of String where each element is the name of a task file
	// Illegal file names will be filterred.
	private static ArrayList<String> getAllFileNameList(){
		File[] paths = storageFolder.listFiles();
		ArrayList<String> fileNameList = new ArrayList<String>();
		String pathString;
		
		if(isEmptyFolder(paths)){
			return fileNameList;
		}

		for(File path : paths){
			pathString = getFileName(path);
			
			// Check the integrity of the file name
			// If the file name is illegal, that means it is not a task file
			// It may be a file generated by the system, or the command Storage itself
			// Such files are to be filtered.
			if(isLegalFileName(pathString)){
				fileNameList.add(pathString);
			}
		}
	
		return fileNameList;
	}
	
	// 
	public static ArrayList<String> getFileNameList(){
		ArrayList<String> fileNameList = getAllFileNameList();
		logger.log(Level.INFO,"All file names are: "+fileNameList);
		logger.log(Level.INFO,"Starting to remove done task names...");
		fileNameList = removeDoneTask(fileNameList);
		logger.log(Level.INFO,"Undone File Names are: "+fileNameList);
		
		return fileNameList;
	}
	
	public static ArrayList<String> getDoneFileNameList(){
		ArrayList<String> fileNameList = getAllFileNameList();
		logger.log(Level.INFO,"All file names are: "+fileNameList);
		logger.log(Level.INFO,"Starting to remove undone task names...");
		ArrayList<String> doneNameList = removeUndoneTask(fileNameList);
		logger.log(Level.INFO,"Done File Names are: "+fileNameList);
		return doneNameList;
	}
	
	private static ArrayList<String> removeUndoneTask(ArrayList<String> nameList){
		return filterTaskNames(false,nameList);
	}
	
	private static ArrayList<String> removeDoneTask(ArrayList<String> nameList){
		return filterTaskNames(true,nameList);
	}
	
	private static ArrayList<String> filterTaskNames(boolean isDone, ArrayList<String> nameList){
		boolean condition;
		if(nameList.size() == 0){
			return nameList;
		}else{
			ArrayList<String> result = new ArrayList<String>();
			
			for(String name: nameList){

				if(isDone){
					condition = !isTaskDone(name);
				}else{
					condition = isTaskDone(name);
				}
				
				if(condition){
					result.add(name);
				}
			}
			
			return result;
		}
	}
	
	private static boolean isEmptyFolder(File[] file){
		return file == null;
	}
	
	private static String getFileName(File path){
		String pathString = path.getAbsolutePath();
		
		// extract only the file name out of the entire absolute path
		pathString = pathString.replace(storagePath+pathSeparator, Constant.EMPTY_STRING).trim();
		return pathString;
	}

	public static void deleteFile(String fileName) throws IOException{
		fileName = removeSpecialChars(fileName);
		if(isLegalFileName(fileName)){
			Task deletedTask = Storage.getFileContentAsTask(fileName);
			deleteFileWithoutRecord(fileName);
			if(deletedTask.getId().length() != 0){
				modRecord.add(new ModificationRecord(Constant.DELETE_COMMAND_TYPE,Constant.EMPTY_STRING,deletedTask.getId()));
			}
		}
	}
	
	// delete api that will not be recorded in modification record (meant for those operations that do not need to be sync)
	public static void deleteFileWithoutRecord(String fileName){
		fileName = removeSpecialChars(fileName);
		if(isLegalFileName(fileName)){
			while(GlobalFields.IS_GOOGLE_CONNECTED &&
				Storage.isRecordExist(Constant.ADD_COMMAND_TYPE, fileName));
			
			String newPathName = determinePathName(fileName);
			File fileToDelete = new File(newPathName);
			if(fileToDelete.exists()){
				logger.log(Level.INFO,"Deleting file "+fileName);
				fileToDelete.delete();
			}
		}
	}
	
	public static void addFile(Task task) throws IOException{
		String fileName = task.getTaskFileName();
		fileName = removeSpecialChars(fileName);
		addFileWithoutRecord(task);
		modRecord.add(new ModificationRecord(Constant.ADD_COMMAND_TYPE,fileName,Constant.EMPTY_STRING));
	}
	
	// add api that will not be recorded in modification record (meant for those operations that do not need to be sync)
	public static void addFileWithoutRecord(Task task) throws IOException{
		if(task != null){
			String fileName = task.getTaskFileName();
			// removing special characters contained in the file name
			fileName = removeSpecialChars(fileName);
			
			if(isLegalFileName(fileName)){
				logger.log(Level.INFO,"Adding file "+fileName);
				String newPathName = determinePathName(fileName);
				File fileToAdd = new File(newPathName);
		
				fileToAdd.createNewFile();
				FileWriter fileWriter= new FileWriter(fileToAdd);
				String taskContent = getTaskContent(task);
				taskContent += (Constant.NEW_LINE+task.getSyncStatus()+Constant.HEX+task.getTaskStatus()+Constant.HEX+task.getId());
				fileWriter.write(taskContent);
				fileWriter.flush();
				fileWriter.close();
			}else{
				logger.log(Level.INFO,fileName+" is an illegal file name");
			}
		}else{
			logger.log(Level.SEVERE,"Null tasks detected while adding files");
		}
	}
	
	private static String removeSpecialChars(String fileName) {
		fileName = fileName.replaceAll(Constant.SPECIALS_REGEX, Constant.EMPTY_STRING);
		return fileName;
	}

	private static boolean isLegalFileName(String fileName){
		if(isFirstCharDigit(fileName) && isLastCharDigit(fileName)){
			if(checkSpecialSymbols(fileName)){
				return true;
			}else{
				return false;
			}
		}else{
			return false;
		}
	}
	
	private static boolean checkSpecialSymbols(String fileName){
		return !fileName.matches(Constant.SPECIALS_REGEX);
	}
	
	private static boolean isFirstCharDigit(String str){
		char firstChar = str.charAt(0);
		return isDigit(firstChar);
	}
	
	private static boolean isLastCharDigit(String str){
		char lastChar = str.charAt(str.length()-5);
		return isDigit(lastChar);
	}
	
	// check that if a given character is a character representation of a digit
	private static boolean isDigit(char c){
		return (c >= '0')&&(c <= '9');
	}
	
	private static String getTaskContent(Task task){
		return task.toString();
	}
	
	public static Task getFileContentAsTask(String fileName) throws IOException{
		fileName = removeSpecialChars(fileName);
		String newPathName = determinePathName(fileName);
		File file = new File(newPathName);
		if(file.exists()){
			String taskStatusString;
			String fileContent = Constant.EMPTY_STRING;
			String isSync = Constant.EMPTY_STRING;
			String isDone = Constant.EMPTY_STRING;
			String taskId = Constant.EMPTY_STRING;
			
			BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
			// Getting the basic task information line
			fileContent = bufferedReader.readLine();
			
			// Getting the task status line
			taskStatusString = bufferedReader.readLine();
			
			String[] status = taskStatusString.split(Constant.HEX);
			
			isSync = status[0];
			isDone = status[1];
			
			if(status.length >= 3){		
				taskId = status[2];
			}
			
			bufferedReader.close();
			
			Task task = new Task(fileContent);
			initTaskStatus(task, isSync, isDone,taskId);
			return task;
		}else{
			return null;
		}
	}
	
	// Initialize whether the task has been sync to Google Calendar (and if so, what is its id)
	// and whether the task has been done or not
	private static void initTaskStatus(Task task,String isSync, String isDone, String taskId){
		if(isSync.equalsIgnoreCase(Constant.TRUE_STRING)){
			task.setSync();
		}
		if(isDone.equalsIgnoreCase(Constant.TRUE_STRING)){
			task.markDone();
		}
		if(!taskId.equalsIgnoreCase(Constant.EMPTY_STRING)){
			task.setId(taskId);
		}
	}
	
	private static String determinePathName(String fileName){
		String pathName = storagePath+pathSeparator+fileName;
		return pathName;
	}
	
	public static boolean isRecordEmpty(){
		return (modRecord.size() == 0);
	}
	
	public static ModificationRecord getRecord(){
		if(modRecord.size() != 0){
			ModificationRecord record = modRecord.get(0);
			modRecord.remove(0);
			return record;
		}else{
			return null;
		}
	}
	
	// Essentially this class is getting the second line of a task file and splits it into three strings.
	private static String[] getTaskStatus(String fileName){
		fileName = removeSpecialChars(fileName);
		String newPathName = determinePathName(fileName);
		File file = new File(newPathName);
		try {
			BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
			bufferedReader.readLine();
			String taskStatusString = bufferedReader.readLine();
			String[] status = taskStatusString.split(Constant.HEX);
			bufferedReader.close();
			return status;
		} catch (Exception e) {
			// do nothing
		}
		
		return null;
	}
	
	// Check if a task is done by retrieving its information from the file
	private static boolean isTaskDone(String fileName){
		try{
			fileName = removeSpecialChars(fileName);
			String[] taskStatus = getTaskStatus(fileName);
			if(taskStatus.length >= 2){
				String taskDoneState = taskStatus[1];
				return taskDoneState.equalsIgnoreCase(Constant.TRUE_STRING);
			}else{
				return false;
			}
		}catch(Exception e){
			return false;
		}
	}
	
	public static String getTaskIdFromFileName(String fileName){
		fileName = removeSpecialChars(fileName);
		String[] status = getTaskStatus(fileName);
		if(status!=null && status.length == 3){
			return status[2];
		}else{
			return Constant.EMPTY_STRING;
		}
	}
	
	// Check if a file is sync to Google Calendar
	public static boolean isFileSync(String fileName){
		String[] status = getTaskStatus(fileName);
		if(status != null && status.length != 0){
			String syncStatusString = status[0];
			if(syncStatusString == null){
				return false;
			}
			
			if(syncStatusString.equalsIgnoreCase(Constant.TRUE_STRING)){
				return true;
			}else if(syncStatusString.equalsIgnoreCase(Constant.FALSE_STRING)){
				return false;
			}else{
				return false;
			}
		}else{
			return false;
		}
	}
	
	// Check if a record exists in the modRecord
	// If commandType is an add command, attribute is expected to be the file name added;
	// If commandType is a delete command, attribute is expected to be the task Id deleted.
	public static boolean isRecordExist(String commandType,String attribute){
		if(modRecord.size() <= 0){
			return false;
		}
		
		for(ModificationRecord record: modRecord){
			if(record.getModType().equalsIgnoreCase(commandType)){
				if(commandType.equalsIgnoreCase(Constant.ADD_COMMAND_TYPE)){
					String attr = record.getAddedFileName();
					if(attr.equalsIgnoreCase(attribute)){
						return true;
					}
				}else if(commandType.equalsIgnoreCase(Constant.DELETE_COMMAND_TYPE)){
					String attr = record.getDeletedTaskId();
					if(attr.equalsIgnoreCase(attribute)){
						return true;
					}
				}else{
					return false;
				}
			}
		}
		
		return false;
	}
	
	// Store all the records in modRecord into the commandStorage file
	// so that the next time the program launches, these commands will get synchronized.
	public static void storeRecord(){
		if(!isRecordEmpty()){
			ModificationRecord record;
			String recordString;
			Iterator<ModificationRecord> it = modRecord.iterator();
			
			emptyFile(commandStorage);
			
			try {
				FileWriter writer = new FileWriter(commandStorage);

				while(it.hasNext()){
					record = it.next();
					recordString = record.getModType()+Constant.HEX
									+record.getAddedFileName()+Constant.HEX
									+record.getDeletedTaskId();
					logger.log(Level.INFO,"Storing command: "+recordString);
					writer.append(recordString+Constant.NEW_LINE);
				}
				
				writer.flush();
				writer.close();
			} catch (IOException e) {
				// do nothing
			}
		}else{
			emptyFile(commandStorage);
		}
	}
	
	private static void emptyFile(File file){
		try {
			@SuppressWarnings("resource")
			PrintWriter pWriter = new PrintWriter(file);
			pWriter.print(Constant.EMPTY_STRING);
		} catch (FileNotFoundException e1) {
			logger.log(Level.SEVERE,"File not found when checking emptyFile for "+file.getName());
		}
	}
}